package smtpd

import (
	"crypto/tls"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"net/textproto"
	"strconv"
	"strings"

	"git.oschina.net/chengzishuai/postman/src/net/smtpd/dkim"
	"git.oschina.net/chengzishuai/postman/src/net/smtpd/mail"

	"git.oschina.net/chengzishuai/postman/src/net/smtpd/dns"
)

type Client struct {
	Text       *textproto.Conn
	conn       net.Conn
	serverName string
	localName  string
	didHello   bool
	helloError error
	ext        map[string]string
	tls        bool
	didConn    bool
}

var testHookStartTLS func(*tls.Config)

type dataCloser struct {
	c *Client
	io.WriteCloser
}

func (c *Client) Data() (io.WriteCloser, error) {
	_, _, err := c.cmd(354, "DATA")
	if err != nil {
		return nil, err
	}
	return &dataCloser{c, c.Text.DotWriter()}, nil
}

func (d *dataCloser) Close() error {
	d.WriteCloser.Close()
	id, msg, err := d.c.Text.ReadResponse(250)
	if id != 250 {
		return errors.New("smtpd: " + strconv.Itoa(id) + " " + msg)
	}
	return err
}

//
func Dial(addr string) (*Client, error) {
	host, port, err := net.SplitHostPort(addr)
	if err != nil {
		port = "25"
	}
	conn, err := net.Dial("tcp", host+":"+port)
	if err != nil {
		return nil, err
	}
	return NewClient(conn, host)
}

//
func NewClient(conn net.Conn, host string) (*Client, error) {
	text := textproto.NewConn(conn)
	_, _, err := text.ReadResponse(220)
	if err != nil {
		text.Close()
		return nil, err
	}
	c := &Client{Text: text, conn: conn, serverName: host, localName: "localhost", didConn: true}
	return c, nil
}

//
func (c *Client) Close() error {
	return c.Text.Close()
}

// cmd is a convenience function that sends a command and returns the response
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
	id, err := c.Text.Cmd(format, args...)
	if err != nil {
		return 0, "", err
	}
	c.Text.StartResponse(id)
	defer c.Text.EndResponse(id)
	code, msg, err := c.Text.ReadResponse(expectCode)
	return code, msg, err
}
func (c *Client) hello() error {
	if !c.didHello {
		c.didHello = true
		err := c.ehlo()
		if err != nil {
			err := c.helo()
			c.helloError = err
		}
	}
	return c.helloError
}

//
func (c *Client) helo() error {
	_, _, err := c.cmd(250, "HELO %s", c.localName)
	return err
}

//
func (c *Client) ehlo() error {
	_, msg, err := c.cmd(250, "EHLO %s", c.localName)
	if err != nil {
		return err
	}
	ext := make(map[string]string, 0)
	extList := strings.Split(msg, "\n")
	if len(extList) > 1 {
		extList = extList[1:]
		for _, line := range extList {
			args := strings.SplitN(line, " ", 2)
			if len(args) > 1 {
				ext[args[0]] = args[1]
			} else {
				ext[args[0]] = ""
			}
		}
	}
	c.ext = ext
	return err
}

//
func (c *Client) Hello() error {
	if c.didHello {
		return errors.New("smtpd: Hello called after other methods")
	}
	return c.hello()
}

//
func (c *Client) Extension(ext string) (bool, string) {
	if err := c.hello(); err != nil {
		return false, ""
	}
	if c.ext == nil {
		return false, ""
	}
	ext = strings.ToUpper(ext)
	param, ok := c.ext[ext]
	return ok, param
}

//
func (c *Client) StartTLS(config *tls.Config) error {
	if err := c.hello(); err != nil {
		return err
	}
	id, msg, err := c.cmd(220, "STARTTLS")
	if err != nil {
		return errors.New("smtpd: " + strconv.Itoa(id) + " " + msg)
	}
	c.conn = tls.Client(c.conn, config)
	c.Text = textproto.NewConn(c.conn)
	c.tls = true
	return c.ehlo()
}

//
func (c *Client) Mail(from string) error {
	if err := c.hello(); err != nil {
		return err
	}
	cmdStr := "MAIL FROM:<%s>"
	id, msg, err := c.cmd(250, cmdStr, from)
	if id != 250 {
		err = errors.New("smtpd: " + strconv.Itoa(id) + " " + msg)
	}
	return err
}

//
func (c *Client) Rcpt(to string) error {
	id, msg, err := c.cmd(25, "RCPT TO:<%s>", to)
	if id != 250 {
		err = errors.New("smtpd: " + strconv.Itoa(id) + " " + msg)
	}
	return err
}

//
func (c *Client) Reset() error {
	if err := c.hello(); err != nil {
		return err
	}
	_, _, err := c.cmd(250, "RSET")
	return err
}

//
func (c *Client) Quit() error {
	if err := c.hello(); err != nil {
		return err
	}
	_, _, err := c.cmd(221, "QUIT")
	if err != nil {
		return err
	}
	return c.Text.Close()
}

//
func (c *Client) SendMail(m mail.Mail) error {
	if !c.didConn {
		mx := dns.NewMxData(m.To)
		host, err := mx.LookUpMx()
		if err != nil {
			return err
		}
		c, err = Dial(host + ":25")
		if err != nil {
			return err
		}
	}
	defer c.Close()
	var err error
	if err = c.hello(); err != nil {
		return err
	}

	if ok, _ := c.Extension("STARTTLS"); ok {
		config := &tls.Config{ServerName: c.serverName, InsecureSkipVerify: true}
		if testHookStartTLS != nil {
			testHookStartTLS(config)
		}
		if err = c.StartTLS(config); err != nil {
			return err
		}
	}
	if err = c.Mail(m.From); err != nil {
		return err
	}
	if err = c.Rcpt(m.To); err != nil {
		return err
	}
	w, err := c.Data()
	if err != nil {
		return err
	}
	sig := dkim.New(m)
	sig.GetPrivateKey()
	sig.GetSignature()
	str := fmt.Sprintf(mail.MailTemplate, m.Date.Format("Monday, 02-Jan-06 15:04:05 (UTC)"), m.From, m.To, m.Subject, sig.Signature(), m.Body)
	log.Println(str)
	w.Write([]byte(str))
	err = w.Close()
	if err != nil {
		return err
	}
	return c.Quit()

	return err
}
